//Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;

namespace Microsoft.Office.OneNote
{
	/// <summary>
	/// Abstract base class which all nodes (notebooks, sections, pages, 
	/// pageobjects, data etc.) in the hierarchy "tree" extend for common 
	/// functionality, such as serialization -- and should generally not 
	/// be directly accessed.
	/// </summary>
	[Serializable]
	public abstract class ANode : ICloneable
	{
		/// <summary>
		/// Default constructor for a new node in the import tree.
		/// </summary>
		protected ANode()
		{
		}

		/// <summary>
		/// ANodes need to be cloneable so that implicit memory management
		/// can occurr when adding a node to the import tree.
		/// </summary>
		/// <returns></returns>
		public abstract object Clone();

		/// <summary>
		/// Ensures our properties are in sync.
		/// </summary>
		public void EnsurePropertiesSynchronized()
		{
			EnsurePropertiesSynchronized(false /* force */);
		}

		/// <summary>
		/// Ensures our properties are in sync.
		/// </summary>
		public virtual void EnsurePropertiesSynchronized(bool force)
		{
			// Do nothing by default.
		}

		/// <summary>
		/// Tries to retrieves the property with the given name.
		/// </summary>
		/// <typeparam name="PropertyType">The type of the property to be retrieved.</typeparam>
		/// <param name="propertyName">The property to be retrieved.</param>
		/// <param name="propertyValue">The output value of the given type.</param>
		/// <returns>true on success</returns>
		public virtual bool TryToGetProperty<PropertyType>(string propertyName, out PropertyType propertyValue)
		{
			EnsurePropertiesSynchronized();
			if (properties.ContainsKey(propertyName))
			{
				Object value = properties[propertyName];
				if (value is PropertyType)
				{
					propertyValue = (PropertyType) value;
					return true;
				}
			}

			propertyValue = default(PropertyType);
			return false;
		}

		/// <summary>
		/// Returns the property with the given name if it's present and of the
		/// correct type.  Otherwise, returns the specified default.
		/// </summary>
		/// <typeparam name="PropertyType">The type of the property to be 
		///		retrieved.</typeparam>
		/// <param name="propertyName">The property to be retrieved.</param>
		/// <param name="defaultValue">The default value to be returned if the 
		///		property isn't present or isn't of the correct type.</param>
		/// <returns></returns>
		public PropertyType GetProperty_Else<PropertyType>(string propertyName, PropertyType defaultValue)
		{
			PropertyType value;
			if (TryToGetProperty(propertyName, out value))
				return value;
			else
				return defaultValue;
		}

		/// <summary>
		/// Sets the given property to the specified value.
		/// </summary>
		/// <param name="propertyName">The name of the property to set.</param>
		/// <param name="propertyValue">The value to set this property to.</param>
		public virtual void SetProperty(string propertyName, Object propertyValue)
		{
			bool propertyModified = false;
			Object oldPropertyValue = null;

			if (propertyValue == null && properties.ContainsKey(propertyName))
			{
				properties.Remove(propertyName);
				propertyModified = true;
			}
			else if (propertyValue != null)
			{
				if (!properties.TryGetValue(propertyName, out oldPropertyValue) ||
					!oldPropertyValue.Equals(propertyValue))
				{
					properties[propertyName] = propertyValue;
					propertyModified = true;
				}
			}

			// Perform any appropriate post-processing:
			if (propertyModified)
				OnPropertyModified(propertyName, oldPropertyValue, propertyValue);
		}

		/// <summary>
		/// Called after a property has been modified with the property, old value and new value.
		/// </summary>
		/// <param name="propertyName">The name of the modified property.</param>
		/// <param name="oldPropertyValue">The old value of the property.</param>
		/// <param name="newPropertyValue">The new value of the property.</param>
		protected virtual void OnPropertyModified(string propertyName, Object oldPropertyValue, Object newPropertyValue)
		{
			// By default, do nothing.
		}

		/// <summary>
		/// Have we loaded our children yet?  Note that this doesn't guarantee that
		/// the children is in sync -- just that it's been loaded ever.
		/// </summary>
		/// <returns>True if this node has loaded our children.</returns>
		public virtual bool AreChildrenLoaded()
		{
			return true;
		}

		/// <summary>
		/// Ensures our immediate children are in sync.  If we've synchronized lately,
		/// this call may be optimized or skipped.
		/// </summary>
		public void EnsureChildrenSynchronized()
		{
			EnsureChildrenSynchronized(false /* deep */);
		}

		/// <summary>
		/// Ensures all of our immediate and non-immediate children are in sync.
		/// If we've synchronized lately, this call may be optimized or skipped
		/// </summary>
		public void EnsureChildrenSynchronized(bool deep)
		{
			EnsureChildrenSynchronized(deep, false /* force */);
		}

		/// <summary>
		/// Ensures all of our immediate and non-immediate children are in sync.
		/// If we've synchronized lately, this call may be optimized or skipped 
		/// if force is set to false.
		/// </summary>
		public virtual void EnsureChildrenSynchronized(bool deep, bool force)
		{
			// By default, do nothing...
		}

		/// <summary>
		/// Tests to see if this node can contain the specified child node.
		/// </summary>
		/// <param name="child">The child which we are testing.</param>
		/// <returns>Returns true if this node can contain the specified child.</returns>
		public virtual bool CanContain(ANode child)
		{
			return true;
		}

		/// <summary>
		/// Adds the specified node as a child of this node in the import tree.
		/// </summary>
		/// <param name="child">The child node.</param>
		public void AppendChild(ANode child)
		{
			InsertChild(child, GetChildCount());
		}

		/// <summary>
		/// Inserts the given node as a child at a specific position.
		/// </summary>
		/// <param name="child">The child node to be added.</param>
		/// <param name="index">The position where this child should be added.</param>
		/// <returns></returns>
		public virtual ANode InsertChild(ANode child, int index)
		{
			if (!this.CanContain(child))
			{
				throw (new OneNoteException("Specified child node '" + child + "' cannot be inserted here."));
			}

			if (child.Parent == this)
			{
				// We're reordering our children:
				children.Remove(child);
			}
			else if (child.Parent != null)
			{
				child = (ANode) child.Clone();
			}

			child.parentNode = this;
			children.Insert(index, child);

			return child;
		}

		/// <summary>
		/// Counts the number of child nodes.
		/// </summary>
		public virtual int GetChildCount()
		{
			EnsureChildrenSynchronized();
			return children.Count;
		}

		/// <summary>
		/// Retrieves the child as the specified index.
		/// </summary>
		/// <param name="index">The 0-based index of the child to be return.</param>
		/// <returns>The ANode at the specified index.</returns>
		public virtual ANode GetChild(int index)
		{
			return (ANode) children[index];
		}

		/// <summary>
		/// Deletes the specified child.
		/// </summary>
		/// <param name="child">The ANode to be removed.</param>
		public virtual void DeleteChild(ANode child)
		{
			Debug.Assert(child.Parent == this);
			Debug.Assert(children.Contains(child));

			children.Remove(child);
			child.parentNode = null;
		}

		/// <summary>
		/// All nodes that can be imported into OneNote need to be able to 
		/// serialize themselves into their appropriate XML representation.
		/// </summary>
		/// <param name="parentNode">
		/// A reference to the parent XML node that we should serialize ourselves
		/// as a child of.
		/// </param>
		/// <returns>
		/// The serialized node.
		/// </returns>
		protected internal XmlNode SerializeToXml(XmlNode parentNode)
		{
			return SerializeToXml(parentNode, true /* deep */);
		}

		/// <summary>
		/// All nodes that can be imported into OneNote need to be able to 
		/// serialize themselves into their appropriate XML representation.
		/// </summary>
		/// <param name="parentNode">
		/// A reference to the parent XML node that we should serialize ourselves
		/// as a child of.
		/// </param>
		/// <param name="deep">
		/// Should we recursively serialize our children?
		/// </param>
		/// <returns>
		/// The serialized node.
		/// </returns>
		protected internal abstract XmlNode SerializeToXml(XmlNode parentNode, bool deep);

		/// <summary>
		/// All nodes that can be exported from OneNote need to be able to 
		/// deserialize themselves from their appropriate XML representation.
		/// </summary>
		/// <param name="node">
		/// A reference to the XML node that we should deserialize ourselves
		/// from.
		/// </param>
		protected internal abstract void DeserializeFromXml(XmlNode node);

		/// <summary>
		/// A reference to our parent ANode in import tree.
		/// </summary>
		public ANode Parent
		{
			get
			{
				return parentNode;
			}
			set
			{
				this.parentNode = value;
			}
		}

		/// <summary>
		/// Returns true if this node is connected via a path of ancestors to 
		/// the notebook root.
		/// </summary>
		public bool IsConnected
		{
			get
			{
				if (this.parentNode != null)
					return this.parentNode.IsConnected;
				else
					return (this is NotebookRoot);
			}
		}

		protected ANode parentNode;
		protected List<ANode> children = new List<ANode>();
		protected Dictionary<string, Object> properties = new Dictionary<string, Object>();
	}
}